spb/spboucher.ai Public
spboucher.ai — personal website of Simon-Pierre Boucher.
TypeScript 93.4%
HTML 5.5%
CSS 1%
1/*2 page.tsx3 spboucher.ai Web4 Author: Simon-Pierre Boucher5 Mail: contact@spboucher.ai6*/78import type { Metadata } from "next";9import Image from "next/image";10import Link from "next/link";11import { notFound } from "next/navigation";12import {13 ArrowLeft,14 ArrowRight,15 CalendarDays,16 Clock,17 Mail,18} from "lucide-react";1920import { Badge } from "@/components/ui/badge";21import { BlogContent } from "@/components/blog-content";22import { Reveal } from "@/components/reveal";23import { getBlogPost, getBlogPosts, getBlogSlugs } from "@/lib/blog";2425interface PageProps {26 params: Promise<{ slug: string }>;27}2829export function generateStaticParams() {30 return getBlogSlugs().map((slug) => ({ slug }));31}3233export async function generateMetadata({34 params,35}: PageProps): Promise<Metadata> {36 const { slug } = await params;37 const post = getBlogPost(slug);38 if (!post) return {};39 return {40 title: post.title,41 description: post.excerpt,42 openGraph: {43 title: post.title,44 description: post.excerpt,45 type: "article",46 publishedTime: post.date,47 authors: ["Simon-Pierre Boucher"],48 },49 };50}5152function AuthorSignature({ compact = false }: { compact?: boolean }) {53 return (54 <div className="flex items-center gap-4">55 <Image56 src="/profile/simon-pierre-boucher.jpeg"57 alt="Portrait of Simon-Pierre Boucher"58 width={96}59 height={96}60 className={`${compact ? "h-11 w-11" : "h-14 w-14"} rounded-full object-cover object-top ring-2 ring-primary/20`}61 />62 <div className="min-w-0">63 <p className="font-display text-sm font-semibold tracking-tight">64 Simon-Pierre Boucher65 </p>66 {!compact && (67 <p className="text-xs text-muted-foreground">68 Professor — Department of Administrative Sciences, UQO69 </p>70 )}71 <a72 href="mailto:contact@spboucher.ai"73 className="mt-0.5 inline-flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-primary"74 >75 <Mail className="h-3 w-3" aria-hidden="true" />76 contact@spboucher.ai77 </a>78 </div>79 </div>80 );81}8283export default async function BlogPostPage({ params }: PageProps) {84 const { slug } = await params;85 const post = getBlogPost(slug);86 if (!post) notFound();8788 const posts = getBlogPosts();89 const index = posts.findIndex((p) => p.slug === slug);90 const previous = index > 0 ? posts[index - 1] : undefined;91 const next = index < posts.length - 1 ? posts[index + 1] : undefined;9293 return (94 <article className="mx-auto max-w-3xl px-4 py-16 sm:px-6">95 <Reveal>96 <Link97 href="/blog"98 className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-primary"99 >100 <ArrowLeft className="h-4 w-4" aria-hidden="true" />101 All essays102 </Link>103 </Reveal>104105 {/* Header */}106 <Reveal className="mt-8">107 <header>108 <div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs font-medium text-muted-foreground">109 <Badge>Essay</Badge>110 <span className="inline-flex items-center gap-1.5">111 <CalendarDays112 className="h-3.5 w-3.5 text-primary/70"113 aria-hidden="true"114 />115 {post.dateLabel}116 </span>117 <span className="inline-flex items-center gap-1.5">118 <Clock className="h-3.5 w-3.5 text-primary/70" aria-hidden="true" />119 {post.readingMinutes} min read120 </span>121 <span className="flex flex-wrap gap-1.5">122 {post.tags.map((tag) => (123 <Badge key={tag} variant="outline">124 {tag}125 </Badge>126 ))}127 </span>128 </div>129 <h1 className="mt-5 font-display text-3xl font-bold leading-tight tracking-tight sm:text-[2.75rem] sm:leading-[1.15]">130 {post.title}131 </h1>132 <div className="mt-7 border-y border-border/60 py-4">133 <AuthorSignature compact />134 </div>135 </header>136 </Reveal>137138 {/* Body */}139 <Reveal delay={0.08} className="mt-10">140 <BlogContent blocks={post.blocks} />141 </Reveal>142143 {/* Signature */}144 <Reveal className="mt-14">145 <footer className="border-y-2 border-foreground bg-card/50 p-6 sm:p-8">146 <p className="flex items-center gap-3 font-mono text-[0.68rem] font-medium uppercase tracking-[0.22em] text-primary">147 <span aria-hidden="true" className="h-px w-6 bg-primary/60" />148 Written by149 </p>150 <div className="mt-4">151 <AuthorSignature />152 </div>153 </footer>154 </Reveal>155156 {/* Prev / next */}157 {(previous || next) && (158 <Reveal className="mt-8">159 <nav160 aria-label="More essays"161 className="grid gap-3 sm:grid-cols-2"162 >163 {previous ? (164 <Link165 href={`/blog/${previous.slug}`}166 className="group rounded-3xl border border-border/60 bg-card/70 p-5 shadow-[var(--shadow-soft)] transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/25"167 >168 <span className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground">169 <ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />170 Previous essay171 </span>172 <p className="mt-2 font-display text-sm font-semibold leading-snug tracking-tight transition-colors group-hover:text-primary">173 {previous.title}174 </p>175 </Link>176 ) : (177 <span aria-hidden="true" />178 )}179 {next && (180 <Link181 href={`/blog/${next.slug}`}182 className="group rounded-3xl border border-border/60 bg-card/70 p-5 text-right shadow-[var(--shadow-soft)] transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/25"183 >184 <span className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground">185 Next essay186 <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />187 </span>188 <p className="mt-2 font-display text-sm font-semibold leading-snug tracking-tight transition-colors group-hover:text-primary">189 {next.title}190 </p>191 </Link>192 )}193 </nav>194 </Reveal>195 )}196 </article>197 );198}199